<?php

/**
 * CCodeModel class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CCodeModel is the base class for model classes that are used to generate code.
 *
 * Each code generator should have at least one code model class that extends from this class.
 * The purpose of a code model is to represent user-supplied parameters and use them to
 * generate customized code.
 *
 * Derived classes should implement the {@link prepare} method whose main task is to
 * fill up the {@link files} property based on the user parameters.
 *
 * The {@link files} property should be filled with a set of {@link CCodeFile} instances,
 * each representing a single code file to be generated.
 *
 * CCodeModel implements the feature of "sticky attributes". A sticky attribute is an attribute
 * that can remember its last valid value, even if the user closes his browser window
 * and reopen it. To declare an attribute is sticky, simply list it in a validation rule with
 * the validator name being "sticky".
 *
 * @property array $templates A list of available code templates (name=>directory).
 * @property string $templatePath The directory that contains the template files.
 * @property string $stickyFile The file path that stores the sticky attribute values.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.gii
 * @since 1.1.2
 */
abstract class CCodeModel extends CFormModel {

    const STATUS_NEW = 1;
    const STATUS_PREVIEW = 2;
    const STATUS_SUCCESS = 3;
    const STATUS_ERROR = 4;

    static $keywords = array(
        '__class__',
        '__dir__',
        '__file__',
        '__function__',
        '__line__',
        '__method__',
        '__namespace__',
        'abstract',
        'and',
        'array',
        'as',
        'break',
        'case',
        'catch',
        'cfunction',
        'class',
        'clone',
        'const',
        'continue',
        'declare',
        'default',
        'die',
        'do',
        'echo',
        'else',
        'elseif',
        'empty',
        'enddeclare',
        'endfor',
        'endforeach',
        'endif',
        'endswitch',
        'endwhile',
        'eval',
        'exception',
        'exit',
        'extends',
        'final',
        'final',
        'for',
        'foreach',
        'function',
        'global',
        'goto',
        'if',
        'implements',
        'include',
        'include_once',
        'instanceof',
        'interface',
        'isset',
        'list',
        'namespace',
        'new',
        'old_function',
        'or',
        'parent',
        'php_user_filter',
        'print',
        'private',
        'protected',
        'public',
        'require',
        'require_once',
        'return',
        'static',
        'switch',
        'this',
        'throw',
        'try',
        'unset',
        'use',
        'var',
        'while',
        'xor',
    );

    /**
     * @var array user confirmations on whether to overwrite existing code files with the newly generated ones.
     * The value of this property is internally managed by this class and {@link CCodeGenerator}.
     */
    public $answers;

    /**
     * @var string the name of the code template that the user has selected.
     * The value of this property is internally managed by this class and {@link CCodeGenerator}.
     */
    public $template;

    /**
     * @var array a list of {@link CCodeFile} objects that represent the code files to be generated.
     * The {@link prepare()} method is responsible to populate this property.
     */
    public $files = array();

    /**
     * @var integer the status of this model. T
     * The value of this property is internally managed by {@link CCodeGenerator}.
     */
    public $status = self::STATUS_NEW;
    private $_stickyAttributes = array();

    /**
     * Prepares the code files to be generated.
     * This is the main method that child classes should implement. It should contain the logic
     * that populates the {@link files} property with a list of code files to be generated.
     */
    abstract public function prepare();

    /**
     * Declares the model validation rules.
     * Child classes must override this method in the following format:
     * <pre>
     * return array_merge(parent::rules(), array(
     *     ...rules for the child class...
     * ));
     * </pre>
     * @return array validation rules
     */
    public function rules() {
        return array(
            array('template', 'required'),
            array('template', 'validateTemplate', 'skipOnError' => true),
            array('template', 'sticky'),
        );
    }

    /**
     * Validates the template selection.
     * This method validates whether the user selects an existing template
     * and the template contains all required template files as specified in {@link requiredTemplates}.
     * @param string $attribute the attribute to be validated
     * @param array $params validation parameters
     */
    public function validateTemplate($attribute, $params) {
        $templates = $this->templates;
        if (!isset($templates[$this->template]))
            $this->addError('template', 'Invalid template selection.');
        else {
            $templatePath = $this->templatePath;
            foreach ($this->requiredTemplates() as $template) {
                if (!is_file($templatePath . '/' . $template))
                    $this->addError('template', "Unable to find the required code template file '$template'.");
            }
        }
    }

    /**
     * Checks if the named class exists (in a case sensitive manner).
     * @param string $name class name to be checked
     * @return boolean whether the class exists
     */
    public function classExists($name) {
        return class_exists($name, false) && in_array($name, get_declared_classes());
    }

    /**
     * Declares the model attribute labels.
     * Child classes must override this method in the following format:
     * <pre>
     * return array_merge(parent::attributeLabels(), array(
     *     ...labels for the child class attributes...
     * ));
     * </pre>
     * @return array the attribute labels
     */
    public function attributeLabels() {
        return array(
            'template' => 'Code Template',
        );
    }

    /**
     * Returns a list of code templates that are required.
     * Derived classes usually should override this method.
     * @return array list of code templates that are required. They should be file paths
     * relative to {@link templatePath}.
     */
    public function requiredTemplates() {
        return array();
    }

    /**
     * Saves the generated code into files.
     */
    public function save() {
        $result = true;
        foreach ($this->files as $file) {
            if ($this->confirmed($file))
                $result = $file->save() && $result;
        }
        return $result;
    }

    /**
     * Returns the message to be displayed when the newly generated code is saved successfully.
     * Child classes should override this method if the message needs to be customized.
     * @return string the message to be displayed when the newly generated code is saved successfully.
     */
    public function successMessage() {
        return 'The code has been generated successfully.';
    }

    /**
     * Returns the message to be displayed when some error occurred during code file saving.
     * Child classes should override this method if the message needs to be customized.
     * @return string the message to be displayed when some error occurred during code file saving.
     */
    public function errorMessage() {
        return 'There was some error when generating the code. Please check the following messages.';
    }

    /**
     * Returns a list of available code templates (name=>directory).
     * This method simply returns the {@link CCodeGenerator::templates} property value.
     * @return array a list of available code templates (name=>directory).
     */
    public function getTemplates() {
        return Yii::app()->controller->templates;
    }

    /**
     * @return string the directory that contains the template files.
     * @throw CException if {@link templates} is empty or template selection is invalid
     */
    public function getTemplatePath() {
        $templates = $this->getTemplates();
        if (isset($templates[$this->template]))
            return $templates[$this->template];
        elseif (empty($templates))
            throw new CHttpException(500, 'No templates are available.');
        else
            throw new CHttpException(500, 'Invalid template selection.');
    }

    /**
     * @param CCodeFile $file whether the code file should be saved
     */
    public function confirmed($file) {
        return $this->answers === null && $file->operation === CCodeFile::OP_NEW || is_array($this->answers) && isset($this->answers[md5($file->path)]);
    }

    /**
     * Generates the code using the specified code template file.
     * This method is manly used in {@link generate} to generate code.
     * @param string $templateFile the code template file path
     * @param array $_params_ a set of parameters to be extracted and made available in the code template
     * @return string the generated code
     */
    public function render($templateFile, $_params_ = null) {
        if (!is_file($templateFile))
            throw new CException("The template file '$templateFile' does not exist.");

        if (is_array($_params_))
            extract($_params_, EXTR_PREFIX_SAME, 'params');
        else
            $params = $_params_;
        ob_start();
        ob_implicit_flush(false);
        require($templateFile);
        return ob_get_clean();
    }

    /**
     * @return string the code generation result log.
     */
    public function renderResults() {
        $output = 'Generating code using template "' . $this->templatePath . "\"...\n";
        foreach ($this->files as $file) {
            if ($file->error !== null)
                $output.="<span class=\"error\">generating {$file->relativePath}<br/>           {$file->error}</span>\n";
            elseif ($file->operation === CCodeFile::OP_NEW && $this->confirmed($file))
                $output.=' generated ' . $file->relativePath . "\n";
            elseif ($file->operation === CCodeFile::OP_OVERWRITE && $this->confirmed($file))
                $output.=' overwrote ' . $file->relativePath . "\n";
            else
                $output.='   skipped ' . $file->relativePath . "\n";
        }
        $output.="done!\n";
        return $output;
    }

    /**
     * The "sticky" validator.
     * This validator does not really validate the attributes.
     * It actually saves the attribute value in a file to make it sticky.
     * @param string $attribute the attribute to be validated
     * @param array $params the validation parameters
     */
    public function sticky($attribute, $params) {
        if (!$this->hasErrors())
            $this->_stickyAttributes[$attribute] = $this->$attribute;
    }

    /**
     * Loads sticky attributes from a file and populates them into the model.
     */
    public function loadStickyAttributes() {
        $this->_stickyAttributes = array();
        $path = $this->getStickyFile();
        if (is_file($path)) {
            $result = @include($path);
            if (is_array($result)) {
                $this->_stickyAttributes = $result;
                foreach ($this->_stickyAttributes as $name => $value) {
                    if (property_exists($this, $name) || $this->canSetProperty($name))
                        $this->$name = $value;
                }
            }
        }
    }

    /**
     * Saves sticky attributes into a file.
     */
    public function saveStickyAttributes() {
        $path = $this->getStickyFile();
        @mkdir(dirname($path), 0755, true);
        file_put_contents($path, "<?php\nreturn " . var_export($this->_stickyAttributes, true) . ";\n");
    }

    /**
     * @return string the file path that stores the sticky attribute values.
     */
    public function getStickyFile() {
        return Yii::app()->runtimePath . '/gii-' . Yii::getVersion() . '/' . get_class($this) . '.php';
    }

    /**
     * Converts a word to its plural form.
     * Note that this is for English only!
     * For example, 'apple' will become 'apples', and 'child' will become 'children'.
     * @param string $name the word to be pluralized
     * @return string the pluralized word
     */
    public function pluralize($name) {
        $rules = array(
            '/(m)ove$/i' => '\1oves',
            '/(f)oot$/i' => '\1eet',
            '/(c)hild$/i' => '\1hildren',
            '/(h)uman$/i' => '\1umans',
            '/(m)an$/i' => '\1en',
            '/(t)ooth$/i' => '\1eeth',
            '/(p)erson$/i' => '\1eople',
            '/([m|l])ouse$/i' => '\1ice',
            '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
            '/([^aeiouy]|qu)y$/i' => '\1ies',
            '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
            '/(shea|lea|loa|thie)f$/i' => '\1ves',
            '/([ti])um$/i' => '\1a',
            '/(tomat|potat|ech|her|vet)o$/i' => '\1oes',
            '/(bu)s$/i' => '\1ses',
            '/(ax|test)is$/i' => '\1es',
            '/s$/' => 's',
        );
        foreach ($rules as $rule => $replacement) {
            if (preg_match($rule, $name))
                return preg_replace($rule, $replacement, $name);
        }
        return $name . 's';
    }

    /**
     * Converts a class name into a HTML ID.
     * For example, 'PostTag' will be converted as 'post-tag'.
     * @param string $name the string to be converted
     * @return string the resulting ID
     */
    public function class2id($name) {
        return trim(strtolower(str_replace('_', '-', preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $name))), '-');
    }

    /**
     * Converts a class name into space-separated words.
     * For example, 'PostTag' will be converted as 'Post Tag'.
     * @param string $name the string to be converted
     * @param boolean $ucwords whether to capitalize the first letter in each word
     * @return string the resulting words
     */
    public function class2name($name, $ucwords = true) {
        $result = trim(strtolower(str_replace('_', ' ', preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $name))));
        return $ucwords ? ucwords($result) : $result;
    }

    /**
     * Converts a class name into a variable name with the first letter in lower case.
     * This method is provided because lcfirst() PHP function is only available for PHP 5.3+.
     * @param string $name the class name
     * @return string the variable name converted from the class name
     * @since 1.1.4
     */
    public function class2var($name) {
        $name[0] = strtolower($name[0]);
        return $name;
    }

    /**
     * Validates an attribute to make sure it is not taking a PHP reserved keyword.
     * @param string $attribute the attribute to be validated
     * @param array $params validation parameters
     */
    public function validateReservedWord($attribute, $params) {
        $value = $this->$attribute;
        if (in_array(strtolower($value), self::$keywords))
            $this->addError($attribute, $this->getAttributeLabel($attribute) . ' cannot take a reserved PHP keyword.');
    }

}
